Skip to content

Add support for PyFrozenDict#6174

Open
MatthieuDartiailh wants to merge 18 commits into
PyO3:mainfrom
MatthieuDartiailh:frozendict
Open

Add support for PyFrozenDict#6174
MatthieuDartiailh wants to merge 18 commits into
PyO3:mainfrom
MatthieuDartiailh:frozendict

Conversation

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor

No description provided.

@MatthieuDartiailh
MatthieuDartiailh force-pushed the frozendict branch 2 times, most recently from ee55a45 to fd12ac9 Compare July 2, 2026 20:05
@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

I am a bit at a loss when it comes to the ffi checks. Pointers welcome

@alex

alex commented Jul 2, 2026

Copy link
Copy Markdown
Member

AFAIK the header that contains this in CPython is Include/internal/pycore_dict.h, so you'd need to include that path.

(Not opining on any part of this patch.)

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

Thanks I will try that.

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

So the ffi-check situtation is a bit more complex since accessing the core headers requires Py_BUILD_CORE define. Is this something pyo3 maintainers would be willing to accept ?

@Icxolu Icxolu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not an expert on the FFI layer, but here are some comment from my side.

(I haven't looked at the wrapping code, as we first need to get the FFI bindings correct.)

Comment thread pyo3-ffi/src/cpython/dictobject.rs Outdated
Comment thread pyo3-ffi/src/dictobject.rs Outdated
@davidhewitt

Copy link
Copy Markdown
Member

So the ffi-check situtation is a bit more complex since accessing the core headers requires Py_BUILD_CORE define. Is this something pyo3 maintainers would be willing to accept ?

We should not be using any symbols from Include/internal.

Comment thread pyo3-ffi-check/macro/src/lib.rs Outdated
@MatthieuDartiailh

MatthieuDartiailh commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

I think this should now look better. I squashed the previous change to reduce the noise. so running just the lib test was too light. I will continue tomorrow.

@Icxolu Icxolu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I think we are getting closer. I went through the safe wrapper as well and left some comments below.

Comment thread pyo3-ffi/src/cpython/dictobject.rs Outdated
Comment thread pyo3-ffi/src/cpython/dictobject.rs
Comment thread src/types/frozendict.rs
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I have been fighthing a bit cfg when it comes to the limited API. I will have to look into your suggestions.

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

I did a first pass in addressing the review and got clean clippy and test execution locally. The wrapper is now available without the limited API and the cfg are a bit more tiddy. I will check the CI run tomorrow and see if I missed anything else.

@codspeed-hq

codspeed-hq Bot commented Jul 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 140 untouched benchmarks


Comparing MatthieuDartiailh:frozendict (363cd08) with main (c85bc26)

Open in CodSpeed

@Icxolu Icxolu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, some smaller suggestions and some more cfg massaging is needed, but not too much more until we can land this I would say.

Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/frozendict.rs Outdated
@MatthieuDartiailh
MatthieuDartiailh marked this pull request as ready for review July 10, 2026 22:34
@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

Thanks for the early reviews @Icxolu

I think I now have everything in order (including the cfgs).

@Icxolu Icxolu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, looking good! Just one small comment regarding the constructor after which I think this is good to go.

Comment thread src/types/frozendict.rs Outdated
Comment on lines 55 to 58
pub fn new<'py, T>(py: Python<'py>, iterable: T) -> PyResult<Bound<'py, PyFrozenDict>>
where
T: IntoPyObject<'py>,
err::PyErr: core::convert::From<<T as conversion::IntoPyObject<'py>>::Error>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically the new constructor for similar types takes a Rust iterator. See for example PyFrozenSet or PyTuple. To be consistent I think we should follow that here as well.

I can see that it could be useful to build a PyFrozenDict directly from a Python. The equivalent on dict is called from_sequence, we can use the same here. Similarly we should not make that generic to make users aware that they need to check what they are passing in.


I also saw that for PyFrozenSet we have a PyFrozenSetBuilder. This could be useful for PyFrozenDict as well, out of scope for this PR, but could be a possible followup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the new verstion looks better.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now. Maybe you can leave a TODO that we should look into if the intermediate list is necessary.

I have not seen a from_sequence constructor for when a Python iterable is already available. Do you plan on adding that? I'm also fine if we want to leave it out for now. Just wanted to make sure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though it was not useful but since it was very simple I added it.

Comment thread src/types/frozendict.rs Outdated
Comment thread src/types/mod.rs
@bschoenmaeckers

Copy link
Copy Markdown
Member

As a possible followup we can add support for a PyAnyDict, where both PyDict & PyFrozenDict have a Deref impl to PyAnyDict. This will remove some duplications from both types.

Comment thread pyo3-ffi/src/cpython/dictobject.rs Outdated
Comment thread pyo3-ffi/src/cpython/dictobject.rs Outdated
@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

I rebased and addressed all review comments. Regarding the constructor, given the limited functionality provided by CPython I went with the safe solution of creating an intermediate Python List. A builder may be feasible following PyFrozenSet but it means mutating frozen object which I am even surprised to see working in the set case.

I will try to work in a follow up PR on the PyAnyDict idea this week-end.

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

ruff is unhappy about some files I did not touch...

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

Rebasd on latest main

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

@bschoenmaeckers Could you elaborate a little on ?

As a possible followup we can add support for a PyAnyDict, where both PyDict & PyFrozenDict have a Deref impl to PyAnyDict. This will remove some duplications from both types.

Trying to implement Deref leads into conflict with the PyAny deref and does not match any existing pattern in pyo3 (there is not an AnySet for example). I am willing to work on this but I really not sure how to attack this in way that actually reduces code duplication.

@Tpt

Tpt commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

I am willing to work on this but I really not sure how to attack this in way that actually reduces code duplication

Maybe introduce a PyAnyDictMethods trait and make PyDictMethods and PyFrozenDictMethod extend it

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

I can do that but what I am missing is how to avoid duplicating the implementation (since the PyAnyDict_Methods impl block will be identical for PyAnyDict, PyFrozenDict and PyDict). I could use a macro but I am wondering if @bschoenmaeckers had something else in mind.

@Tpt

Tpt commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

To go into the Deref direction maybe something like:

#[repr(transparent)]
pub struct PyAnyDict(PyAny);

pyobject_native_type_core!(
    PyAnyDict_Check,
   ...
); // Not sure the macro will work, you might have to implement some traits yourself or with more specialized macros (if it's the case, just read pyobject_native_type_core implementation)

impl PyAnyDictMethods for Bound<'_, PyAnyDict> {
   fn ex(&self) {}
}

impl Deref for Bound<'_, PyFrozenDict> {
   type Target = Bound<'_, PyAnyDict>;
   fn deref(&self) -> &Bound<'_, PyAnyDict> { unsafe { self.cast_unchecked() } }
}

But I am not sure if it's actually doable.

The trait option would be to have something like the following instead of the Deref:

impl PyAnyDictMethods for Bound<'_, PyFrozenDict> {
   fn ex(&self) { unsafe { self.cast_unchecked::<PyAnyDict>() }.ex() }
}

@bschoenmaeckers

Copy link
Copy Markdown
Member

@bschoenmaeckers Could you elaborate a little on ?

As a possible followup we can add support for a PyAnyDict, where both PyDict & PyFrozenDict have a Deref impl to PyAnyDict. This will remove some duplications from both types.

Trying to implement Deref leads into conflict with the PyAny deref and does not match any existing pattern in pyo3 (there is not an AnySet for example). I am willing to work on this but I really not sure how to attack this in way that actually reduces code duplication.

I experimented in this commit: 2aa4eb3.

I had to remove the DerefToPyAny for PyDict to avoid the conflict. But this is fine as it wil dreef trough the anydict to pyany.

If you want you can pick this up and create a mr

@MatthieuDartiailh

Copy link
Copy Markdown
Contributor Author

Thanks @bschoenmaeckers . Since the commit has what looks like many unrelated changes, I will re-apply the idea in a fresh commit. Should that remaion a follow-up or should I include as part of this PR ? And would a similar PR for AnySet make sense ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants